Promise.all
과 Array.fromAsync
목차
개요
Array.fromAsync
가 ES2022에 추가되었을 때는 Promise.all
과 동일한 작업을 하지만 Array.fromAsync
가 좀더 사용하기 편하도록 만든 함수인 줄 알았는데 실제로 사용해보니 많이 달랐다.
두 함수의 공통점, 차이점과 각각의 사용법을 정리해둔다.
공통점
두 함수 모두 여러 Promise
를 한 번에 처리할 수 있도록 도와준다.
Promise.all([Promise.resolve(1), Promise.resolve(2)]) satisfies Promise<number[]>;
Array.fromAsync([Promise.resolve(1), Promise.resolve(2)]) satisfies Promise<number[]>;
배열인 경우 타입으로만 보면 두 함수 모두 Array<Promise<T>>
를 Promise<Array<T>>
형태로 껍데기를 벗겨주는(?) 역할을 한다.
차이점
1. 입력값
Promise.all
의 타입 정의는 다음과 같이 되어 있다.
interface PromiseConstructor {
// lib.es2015.promise.d.ts
/**
* Creates a Promise that is resolved with an array of results when all of the provided Promises
* resolve, or rejected when any Promise is rejected.
* @param values An array of Promises.
* @returns A new Promise.
*/
all<T extends readonly unknown[] | []>(values: T): Promise<{ -readonly [P in keyof T]: Awaited<T[P]>; }>;
// lib.es2015.iterable.d.ts
/**
* Creates a Promise that is resolved with an array of results when all of the provided Promises
* resolve, or rejected when any Promise is rejected.
* @param values An iterable of Promises.
* @returns A new Promise.
*/
all<T>(values: Iterable<T | PromiseLike<T>>): Promise<Awaited<T>[]>;
}
두 타입 정의 모두 단 하나의 인자를 받는다.
Array.fromAsync
는 다음과 같이 정의되어 있다.
interface ArrayConstructor {
/**
* Creates an array from an async iterator or iterable object.
* @param iterableOrArrayLike An async iterator or array-like object to convert to an array.
*/
fromAsync<T>(iterableOrArrayLike: AsyncIterable<T> | Iterable<T | PromiseLike<T>> | ArrayLike<T | PromiseLike<T>>): Promise<T[]>;
/**
* Creates an array from an async iterator or iterable object.
*
* @param iterableOrArrayLike An async iterator or array-like object to convert to an array.
* @param mapfn A mapping function to call on every element of itarableOrArrayLike.
* Each return value is awaited before being added to result array.
* @param thisArg Value of 'this' used when executing mapfn.
*/
fromAsync<T, U>(iterableOrArrayLike: AsyncIterable<T> | Iterable<T> | ArrayLike<T>, mapFn: (value: Awaited<T>, index: number) => U, thisArg?: any): Promise<Awaited<U>[]>;
}
Array.fromAsync
는 최대 두 개의 인자를 받는다.
두번째 인자 mapFn
은 Array.from
과 동일하게 각 요소를 변환하는 함수이다.
또 첫번째 인자도 AsyncIterable<T>
와 ArrayLike<T | PromiseLike<T>>
를 받을 수 있어 커버리지가 더 넓다.
나는 여기까지만 보고 Array.fromAsync
가 Promise.all
의 확장판인 줄 알았다.
하지만 실제로 사용해보니 두 함수의 작동방식이 많이 달랐다.
2. 작동방식
Promise.all
은 입력값으로 받은 Promise
를 병렬로 실행한다.
그에 비해 Array.fromAsync
는 입력값으로 받은 Promise
를 순차적으로 실행한다.
입력값이 Promise<T>[]
형태라면 이미 Promise
들이 모두 실행된 상태이므로 두 함수의 동작 방식은 동일하다.
하지만 입력값이 Iterator<Promise<T>>
등 지연 평가 (lazy evaluation) 되는 경우라면 두 함수의 동작 방식이 달라진다.
const arr = [1, 2, 3];
const sec = () => new Date().getSeconds();
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const secWithDelay = (f: string) => async (i: number) => {
await delay(1000);
console.log(`function: ${f} item: ${i}, sec: ${sec()}`);
};
await Promise.all(Iterator.from(arr).map(secWithDelay("Promise.all")));
console.log("---");
await Array.fromAsync(Iterator.from(arr).map(secWithDelay("Array.fromAsync")));
/*
function: Promise.all item: 1, sec: 43
function: Promise.all item: 2, sec: 43
function: Promise.all item: 3, sec: 43
---
function: Array.fromAsync item: 1, sec: 44
function: Array.fromAsync item: 2, sec: 45
function: Array.fromAsync item: 3, sec: 46
*/
보다시피 Promise.all
은 모든 Promise
를 동시에 실행한 반면, Array.fromAsync
는 각 Promise
가 실행될 때마다 1초씩 지연이 발생했다.
좀더 이해하기 쉽게 두 함수를 명령형으로 직접 구현해보자면 다음과 비슷할 것이다.
async function promiseAll<T>(iter: Iterable<Promise<T>>) {
const result = [];
for (const item of Array.from(iter)) result.push(await item);
return result;
}
그에 비해 Array.fromAsync
는 다음과 같이 구현될 것이다.
async function arrayFromAsync<T>(iter: Iterable<Promise<T>>) {
const result = [];
for (const item of iter) result.push(await item);
return result;
}
Promise.all
은 Array.from(iter)
를 통해 모든 Promise
를 미리 평가한 반면, Array.fromAsync
는 iter
를 순차적으로 순회하면서 각 Promise
를 평가한다.
3. 사용법
이런 차이로 인해 Array.fromAsync
는 입력값으로 받은 Promise
가 순차적으로 실행되므로, Promise
의 실행 순서가 중요할 때 유용하게 사용할 수 있다.
반면 Promise.all
은 모든 Promise
를 병렬로 실행하므로, 병렬적으로 모든 Promise
를 빠르게 처리하고 싶을 때 유용하다.
예시
예를 들어 SNS의 타임라인을 불러오는 경우를 생각해보자. 글은 각각 순서대로 불러와야 하지만, 각 글의 리소스(텍스트, 이미지 등)은 순서가 중요하지 않으므로 병렬로 빠르게 처리하는 것이 좋을 것이다.
const resourceUris = Iterator.from([
Iterator.from([
"https://example.com/post/1/resource/1",
"https://example.com/post/1/resource/2",
]),
Iterator.from([
"https://example.com/post/2/resource/1",
"https://example.com/post/2/resource/2",
]),
...
]);
function fetchPosts(resourceUris: string[][]) {
return Array.fromAsync(resourceUris.map(
(uris) => Promise.all(uris.map(fetch))
));
}
이렇게 하면 각 글의 리소스는 Promise.all
를 통해 병렬로 빠르게 불러오면서도, Array.fromAsync
를 통해 글은 순서대로 불러올 수 있다.
이런 식으로 Array.fromAsync
와 Promise.all
을 적절히 조합하여 사용할 수 있다.
결론
Promise.all
과 Array.fromAsync
는 모두 여러 Promise
를 처리할 수 있도록 도와주는 함수이지만, 입력값의 형태와 작동 방식이 다르다.
단순 배열이라면 큰 상관 없겠지만 Iterator
를 사용한다면 두 함수의 차이를 이해하고 적절히 사용해야 한다.
동시 병렬 처리가 필요한 경우 Promise.all
, 순차적으로 처리해야 하는 경우 Array.fromAsync
를 사용해야 한다.